Validation.php

<?php

namespace Tlf\User;

/**
 *
 * protected functions must return true/false & when calling them, pass an error message as the first arg & an optional callable as the second arg.
 *
 * public functions are called directly and should return `$this`, usually.
 */
class Validation {

    use Validation\SendEmail;

    public $args = [];

    public $lib;
    public $user;
    public $package;

    /** either POST or GET, depending which request method was used */
    public $data = [];
    /** $_POST */
    public $post;
    /** $_GET */
    public $get;


    public $state = true;

    public string $code_user_email;


    public object $post_user_from_email;
    /**
     * @key short identifier for the message
     * @value the message
     */
    public array $messages = [
        'csrf.invalid' => 'Error: CSRF Token invalid. It appears the form was submitted from a different site.',
    ];

    public function __construct($args){
        $this->args = $args;
        $this->lib = $args['lib'];
        $this->user = $args['user'];
        $this->post = $_POST;
        $this->get = $_GET;
        $this->package = $args['package']??null;
    }

    ////////////////////
    //
    // do stuff
    //
    ////////////////////


    /**
     * Log the given action, as long as consent has been given. 
     *
     * @param $action the action to log
     * @param $email the email address to attach the log to or null to use `$_POST['email']`
     * @param $force_consent true to force logging or false to check for user consent before logging
     * @return black hole if consent not given or email not present
     */
    public function log(string $action, ?string $email=null, $force_consent=false){

        if ($email==null)$email = $this->data['email'];

        $user_consents = false;
        if ($force_consent)$user_consents = true;
        else if (isset($this->data['logs_consent'])
            &&$this->data['logs_consent']=='on')$user_consents = true;




        if ($email==null||!$user_consents){
            $this->show_error("To attempt '$action', you must consent to security logs. Please <a href=\"\">click here to refresh</a> & try again.");

            return new \Tlf\User\BlackHole($this);
        }


        $stmt = $this->lib->pdo->prepare($this->lib->queries['user.security_log']);
        $stmt->execute(
            ['action'=>$action,
            'email'=>$email,
            'ip'=>htmlspecialchars($_SERVER['REMOTE_ADDR']),
            'user_agent'=>htmlspecialchars($_SERVER['HTTP_USER_AGENT']??'--no-agent--')
            ]
        );
        // actually do the logging

        return $this;
    }


    /** 
     * @see(\Tlf\User\Lib::enable_csrf()) 
     * @return $this (validation object)
     */
    public function enable_csrf(...$args){
        $this->lib->enable_csrf(...$args);
        return $this;
    }

    /**
     * @param $key_prefix the same key prefix you passed to `$this->enable_csrf()`
     * @param $message a string message or an `@key` to display from $this->messages
     */
    public function check_csrf(string $key_prefix, string $message){
        if ($this->lib->csrf_is_valid($key_prefix)){
            return $this;
        } else {
            $this->show_error($message);
            return new \Tlf\User\BlackHole($this);
        }
    }

    public function show_message(string $message){
        if ($message[0]=='@'){
            $message = $this->messages[substr($message,1)];
        }
        echo '<p>'.$message.'</p>';
        return $this;
    }
    public function show_error(string $message){
        if ($message[0]=='@'){
            $message = $this->messages[substr($message,1)];
        }
        echo '<p class="error">'.$message.'</p>'."\n";
        return $this;
    }

    public function goto($url){
        $this->package->goto('/');
        return $this;
    }

    public function throttle($key, $value, $length=5000){
        $lib = $this->lib;
        $remaining = $lib->throttle_remaining($key,$value);

        if ($remaining>0){
            $seconds = (int)(($remaining + 1000) / 1000);
            echo "<p class=\"error\">Please wait $seconds seconds before trying again.</p>";
            error_log("Throttle for '$key' with value '$value' has '$seconds' remaining");
            return new \Tlf\User\BlackHole($this);
        }

        $lib->add_throttle($key, $value, $length);

        return $this;
    }

    /**
     * @param $form_name string like 'login'
     * @param $csrf_prefix string like 'request-password'
     * @param $csrf_url the target url 
     * @param $csrf_minutes the number of minutes before the token expires
     */
    public function show_csrf_form(string $form_name, string $csrf_prefix, int $csrf_minutes = 30, string $csrf_url=''){
        $this->enable_csrf($csrf_prefix, $csrf_minutes, $csrf_url);
        $this->show_form($form_name);
        return $this;
    }

    public function show_form($form_name){
        echo $this->args['lia']->view('user/form/'.$form_name, $this->args);

        return $this;
        // return true;
    }

    public function set_new_password(string $error='internal error: could not set new password'){
        $password = $this->data['password'];
        $user = $this->lib->user_from_email($this->code_user_email);
        $success_set = $user->new_password($password, $this->args['code']);

        if ($success_set!==true){
            echo '<p class="error">'.$error.'</p>';
            return new \Tlf\User\BlackHole($this);
        }

        // echo '<p>'.$success.'</p>';

        return $this;
    }


    ////////////////////
    //
    // validate stuff
    //
    ////////////////////




    protected function is_get(){

        $is_get = ($_SERVER['REQUEST_METHOD']=='GET');
        if ($is_get)$this->data = $_GET;
        return $is_get;
    }
    protected function is_post(){

        $is_post = ($_SERVER['REQUEST_METHOD']=='POST');
        if ($is_post)$this->data = $_POST;
        return $is_post;
    }

    protected function is_logged_out(){
        return !$this->user->is_logged_in();
    }

    protected function code_is_valid(){
        $stmt = $this->lib->pdo->prepare($this->lib->queries['user.code_is_valid']);
        $stmt->execute(
            ['code'=>$this->args['code'],
            'type'=>$this->args['code_type']
            ]
        );

        $rows = $stmt->fetchAll();

        $success = count($rows)===1;
        if ($success)$this->code_user_email = $rows[0]['email'];

        return $success;
    }

    protected function code_user_is_active(){
        $user = $this->lib->user_from_email($this->code_user_email);
        if ($user->is_active())return true;
        return false;
    }
    protected function code_email_matches_post_email(){
        // var_dump($this->code_user_email);
        // var_dump($this->data['email']);
        // exit;
        return ($this->code_user_email == $this->data['email']);
    }


    protected function post_email_is_valid(){
        if (filter_var($this->data['email'], FILTER_VALIDATE_EMAIL)!==false)return true;   
        return false;
    }

    protected function post_password_is_valid(){
        return $this->lib->is_password_valid($this->data['password']);
    }

    protected function post_password_matches_confirm_password(){
        return $this->data['password'] == $this->data['password_confirm'];
    }

    /** @return true if the account DOES exist */
    protected function post_email_account_exists(){
        $user = $this->lib->user_from_email($this->data['email']);
        $this->post_user_from_email = $user;
        return $user->is_registered();
    }

    /** @return true if the account does NOT exist */
    protected function post_email_account_notexists(){
        return !$this->post_email_account_exists();
    }

    protected function post_email_account_active(){
        if (isset($this->post_user_from_email))return $this->post_user_from_email->is_active();

        $user = $this->lib->user_from_email($this->data['email']);
        $this->post_user_from_email = $user;
        return $user->is_active();
    }

    protected function post_email_confirmed(){
        return $this->data['email'] == $this->data['email_confirm'];
    }

    protected function post_login(){
        $user = $this->lib->user_from_email($_POST['email']);
        if (!$user->is_active)return false;
        $cookie_code = $user->password_login($_POST['password']??null);
        if ($cookie_code==false)return false;
        if (!$user->set_login_cookie($cookie_code))return false;
        $this->user = $user;

        return true;
    }

    protected function agrees_to_terms(){
        if (isset($this->data['agreed_to_terms'])
            &&$this->data['agreed_to_terms']=='on'){
            $this->data['logs_consent']='on';
            return true;
        }
        return false;
    }

    protected function check_honey(){
        if (!isset($this->data['honey']))return false;
        $honey = $this->data['honey'];
        $names = explode(',', $honey);
        if (count($names)!=3)return false;
        if (!isset($this->data[$names[0]])
            ||$this->data[$names[0]]!==''
            )return false;
        if (!isset($this->data[$names[1]])
            ||$this->data[$names[1]]!==''
            )return false;

        if (!isset($this->data[$names[2]])
            ||$this->data[$names[2]]==''
            )return false;

        if (!isset($this->data['honey_answer'])
            ||$this->data['honey_answer'] == ''
            )return false;
        // echo 'no';
        // exit;
        // var_dump($this->data['honey_answer']);
        // var_dump($this->data[$names[2]]);
        // var_dump(password_hash($this->data[$names[2]], PASSWORD_DEFAULT));

        if (!password_verify($this->data[$names[2]], $this->data['honey_answer']))return false;

        return true;
    }


    public function __call($method, $args){
        if (!method_exists($this, $method)){
            throw new \BadMethodCallException("Method $method does not exist.");
        }

        error_log("\n\nCall '$method'");

        $success = $this->$method();
        if ($success)return $this;
        else {
            $this->state = false;
            if (isset($args[0])){
                $this->show_error($args[0]);
            }
            if (isset($args[1])){
                // exit;
                $callable = [array_shift($args[1]), array_shift($args[1])];
                $ret = $callable(...$args[1]);
                if (is_string($ret))echo $ret;
                if (is_object($ret)&&method_exists($ret,'__toString'))echo $ret;
            }
            return new \Tlf\User\BlackHole($this);
        }
    }
}